//
//  NMEventNode.swift
//  NetEaseMusic
//
//  Created by SAGESSE on 2020/1/13.
//  Copyright © 2020 SAGESSE. All rights reserved.
//

import UIKit
import AsyncDisplayKit


class NMEventNode: ASCellNode {
    
    let header: Header
    let tiles: [ASDisplayNode]
    let footer: Footer
    
    /// Whether the autoplay enabled status.
    let playables: [NMEventControllerAutoPlayable]
    
    /// Create a cell for event.
    init(for event: NMEvent) {
        
        // Configure the cell header/contents/footer for event.
        self.header = .init(for: event)
        self.tiles = Display.parse(event.entitys, for: event)
        self.footer = .init(for: event)
        
        // Gets the tile node that supports playback.
        self.playables = self.tiles.flatMap(Self.playable)
        
        super.init()
        
        // Configure the cell all subnodes.
        self.addSubnode(self.header)
        self.tiles.forEach {
            self.addSubnode($0)
        }
        self.addSubnode(self.footer)
    }
    
    /// Setup layout for all subnodes.
    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        let content = ASStackLayoutSpec.vertical().then {
            $0.spacing = 6
            $0.children = tiles
        }

        let spec = ASStackLayoutSpec.vertical(
            header,
            content.insets(left: header.avatar.style.preferredSize.width + 2),
            footer.insets(left: header.avatar.style.preferredSize.width + 2)
        )
        
        return spec.insets(top: 12, left: 12, bottom: 8, right: 12)
    }
    
    /// Enable highlighting now that self.layer has loaded.
    override func didLoad() {
        super.didLoad()
        layer.as_allowsHighlightDrawing = true
    }
    
    override var selectedBackgroundView: UIView? {
        set {}
        get {
            let view = UIView()
            view.backgroundColor = UIColor(white: 0, alpha: 0.2)
            return view
        }
    }
        
    
    /// Convert tile to playable node.
    private static func playable(_ tile: Any) -> [NMEventControllerAutoPlayable] {
        // The forward tile node needs to be expanded.
        if let tile = tile as? NMEventNode.Display.Referenced {
            return tile.tiles.flatMap(playable)
        }

        // Verify that the tile node supports auto playback.
        if let tile = tile as? NMEventControllerAutoPlayable, tile.isPlayable {
            return [tile]
        }
        
        return []
    }
}



// MARK: -


/// Provide the base node.
extension NMEventNode {

    class Header: ASDisplayNode, ASTextNodeDelegate {
        
        let title: NMTextNode = .init()
        let subtitle: ASTextNode = .init()
        let avatar: NMAvatarNode = .init()
        
        /// Create a node for event.
        init(for event: NMEvent) {
            super.init()
            
            self.avatar.url = event.sender.avatar
            self.avatar.auth = event.sender.userType
            self.avatar.pendant = event.sender.pendant
            self.avatar.style.preferredSize = .init(width: 58, height: 58)
            
            self.addSubnode(self.avatar)
            
            self.addSubnode(self.title) {
                
                let name = event.sender.nickname
                let type = NMEvent.namesOfType[event.type] ?? ""
                
                $0.isUserInteractionEnabled = true
                $0.isLayerBacked = false
                $0.delegate = self
                $0.style.flexShrink = 0
                $0.truncationMode = .byTruncatingTail
                $0.maximumNumberOfLines = 1
                $0.attributedText = NSMutableAttributedString(string: "\(name) \(type)：", style: event.theme["event.header.title"]).then {
                    
                    $0.addAttribute(NMTextNode.link, value: name, range: NSMakeRange(0, name.count))
                    $0.addAttribute(.foregroundColor, value: event.theme.color(forKey: "event.link"), range: NSMakeRange(0, name.count))
                }
            }
            
            self.addSubnode(self.subtitle) {
                
//                $0.isUserInteractionEnabled = true
//                $0.isLayerBacked = false
                $0.style.flexShrink = 1
                $0.truncationMode = .byTruncatingTail
                $0.maximumNumberOfLines = 1
                $0.attributedText = NSMutableAttributedString(string: DateFormatter.string(fromSent: event.date), style: event.theme["event.header.subtitle"])
            }
        }
        
        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            let spec = ASStackLayoutSpec.horizontal(
                avatar,
                ASStackLayoutSpec.vertical(
                    ASStackLayoutSpec.horizontal(title, spacing: 2, verticalAlignment: .center),
                    ASStackLayoutSpec.horizontal(subtitle),
                    spacing: 1,
                    verticalAlignment: .center
                ).then {
                    $0.style.flexShrink = 1
                },
                spacing: 2
            )
            return spec
        }
    }
    
    class Display: ASDisplayNode, NMEventNodeDisplayable {
        
        required init(_ entity: NMEvent.Exception, for event: NMEvent) {
            super.init()
            
            guard let exception = try? entity.resolved?(entity, event) as? ASDisplayNode ?? Self.exception(entity, for: event) else {
                return
            }
            
            addSubnode(exception)
        }
        
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            return ASStackLayoutSpec.vertical().then {
                $0.children = subnodes
            }
        }

        /// Parse all content nodes for event.
        static func parse<T>(_ contents: [Any], for event: NMEvent) -> [T] {
            return contents.compactMap {
                // If can't convert to an entity object, ignore this content.
                guard let entity = $0 as? NMEventNodeEntityCompatible else {
                    return nil
                }
                return entity.displayType.init(entity, for: event) as? T
            }
        }
    }

    class Footer: ASDisplayNode {
        
        /// Create a node for event.
        init(for event: NMEvent) {
            super.init()
            
            let str = { (count: Int) -> String? in
                guard count > 0 else {
                    return nil
                }
                return "\(count)"
            }
            
            self.addSubnode(self.making(icon: "cm6_event_footer_repost", value: str(event.forwardings) ?? "转发", event: event))
            self.addSubnode(self.making(icon: "cm6_event_footer_comment", value: str(event.comments) ?? "评论", event: event))
            self.addSubnode(self.making(icon: "cm6_event_footer_unlike", value: str(event.likes) ?? "赞", event: event))
            self.addSubnode(self.making(icon: "cm6_btn_rcmd_more", event: event))
        }
        
        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            
            let spec = ASStackLayoutSpec.horizontal().then {
                $0.justifyContent = .spaceBetween
                $0.alignItems = .center
                $0.children = subnodes
            }
            
            return spec.insets(top: 20, bottom: 20)
        }
        
        /// Create a display items.
        func making(icon: String, value: String? = nil, selector: Selector? = nil, event: NMEvent) -> ASButtonNode {
            let btn = ASButtonNode()
            
            btn.contentSpacing = 5
            
            let it = event.theme.color(forKey: "event.footer.items")
            
            btn.tintColor = it
            btn.setImage(UIImage(named: icon)?.withRenderingMode(.alwaysTemplate), for: .normal)
            
            // If no nedd title, the value will is empty.
            value.map {
                btn.setTitle($0, with: UIFont.systemFont(ofSize: 12), with: it, for: .normal)
            }
            
            // If no need respond, the selector will is empty.
            selector.map {
                btn.addTarget(self, action: $0, forControlEvents: .touchUpInside)
            }
            
            return btn
        }
    }
}


// MARK: -


extension NMEventNode.Display {

    /// The text node for event.
    class Text: ASDisplayNode, ASTextNodeDelegate, NMEventNodeDisplayable {
        
        /// The real text value.
        let value: NMTextNode = .init()
        
        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Text, for event: NMEvent) {
            super.init()

            self.value.isUserInteractionEnabled = true
            self.value.isLayerBacked = false
            self.value.delegate = self
//            self.value.textContainerInset = .init(top: 2, left: 0, bottom: 2, right: 0)
//            self.value.maximumNumberOfLines = 15
//            self.value.truncationAttributedText = NSMutableAttributedString(string: "...", style: event.theme["event.text"])
//            self.value.additionalTruncationMessage = NSMutableAttributedString(string: "查看更多", style: event.theme["event.text"])
            self.value.attributedText = NSMutableAttributedString(string: entity.value(by: event), style: event.theme["event.text"]).then {
                // Configure the line attributes.
                let style = NSMutableParagraphStyle()
                style.lineSpacing = 8
                $0.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, $0.length))
            }
            
            self.addSubnode(self.value)
        }
        
        /// Create a node for entity and event if needed.
        required convenience init?(_ entity: NMEventNodeEntityCompatible, for event: NMEvent) {
            // If the conversion fails, it means incompatibility.
            // If the text is empty, there is no need to create a display node.
            guard let entity = entity as? Entity, event.isReferenced || !entity.value.isEmpty else {
                return nil
            }
            self.init(entity, for: event)
        }

        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            return value.insets()
        }
    }

    /// The image node for event.
    class Image: ASDisplayNode, NMEventNodeDisplayable {
        
        /// The 1 to 3x3 images layout spec.
        class LayoutSpec: ASLayoutSpec {
            
            /// The amount of space between each child.
            var spacing: CGFloat = 0

            /// The size range of single child.
            var singleLayoutSize: ASSizeRange = .init()
            
            /// Asks the layout to return a layout based on given size range.
            override func calculateLayoutThatFits(_ constrainedSize: ASSizeRange) -> ASLayout {
                // Ignore when children is empty or layout spec is empty.
                guard let children = children, !children.isEmpty, constrainedSize.max.width > 0 else {
                    return .init(layoutElement: self, size: constrainedSize.min)
                }
                
                // Adapt content size when there is only one child.
                guard children.count > 1 else {
                    // First calculate out the size of the first child.
                    var size = children[0].layoutThatFits(ASSizeRangeUnconstrained).size
                    
                    // Calculate the minimum size.
                    var minimum = singleLayoutSize.min
                    if minimum.width.isZero || minimum.width.isInfinite {
                        minimum.width = size.width
                    }
                    if minimum.height.isZero || minimum.height.isInfinite {
                        minimum.height = size.height
                    }

                    // Calculate the final scaling.
                    let scale = max(minimum.width / max(size.width, 1), minimum.height / max(size.height, 1))
                    
                    // Calculate the maximum size.
                    var maximum = singleLayoutSize.max
                    if maximum.width.isZero || maximum.width.isInfinite {
                        maximum.width = size.width * scale
                    }
                    if maximum.height.isZero || maximum.height.isInfinite {
                        maximum.height = size.height * scale
                    }

                    // Apply scale and limit size.
                    size.width = min(size.width * scale, min(maximum.width, constrainedSize.max.width))
                    size.height = min(size.height * scale, maximum.height)
                    
                    // Merge everything into the main layout.
                    return .init(layoutElement: children[0], size: size)
                }
                
                // Create a template for each rows.
                let templates = { count -> [(count: Int, column: Int)] in
                    switch count {
                    case 2:  return [(2,2)]
                    case 4:  return [(2,3)]
                    case 5:  return [(2,2),(3,3)]
                    case 7:  return [(3,3),(4,4)]
                    case 8:  return [(4,4)]
                    default: return [(3,3)]
                    }
                }(min(children.count, 9))
                
                // Configuration the environment.
                var index = 0
                var offset = CGFloat.zero
                var layouts = [ASLayout]()
                
                // Iterate through all the sublayouts.
                while index < min(children.count, 9) {
                    // Gets the template being applying.
                    let template = templates[layouts.count % templates.count]
                    let height = (constrainedSize.max.width - (spacing * .init(template.column - 1))) / .init(template.column)
                    
                    // Generate a line of template.
                    let size = CGSize(width: constrainedSize.max.width, height: height)
                    let layout = ASLayout(layoutElement: self, size: size, position: .init(x: 0, y: offset), sublayouts: children[index ..< min(index + template.count, children.count)].enumerated().map {
                        return ASLayout(layoutElement: $1, size: .init(width: height, height: height), position: .init(x: (height + spacing) * .init($0), y: 0), sublayouts: nil)
                    })
                    
                    // And then add sublayout to the rows.
                    layouts.append(layout)
                    offset += height + spacing
                    index += template.0
                }
                
                // Merge everything into the main layout.
                return .init(layoutElement: self, size: .init(width: constrainedSize.max.width, height: max(offset - spacing, 0)), position: .zero, sublayouts: layouts)
            }
        }
        
        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Image, for event: NMEvent) {
            super.init()
            entity.items.forEach {
                let image = NMNetworkImageNode()
                image.style.preferredSize = $0.size
                image.isLayerBacked = true
                image.cornerRadius = 4
                image.url = $0.url
                image.contentMode = .scaleAspectFill
                
                if $0.isGIF {
                    image.badge = Self.badge("动图")
                }
                if $0.isLonger {
                    image.badge = Self.badge("长图")
                    image.contentMode = .top
                }
                
                addSubnode(image)
            }
        }
        
        /// Create a node for entity and event if needed.
        required convenience init?(_ entity: NMEventNodeEntityCompatible, for event: NMEvent) {
            // If the conversion fails, it means incompatibility.
            // If no found images, there is no need to create a display node.
            guard let entity = entity as? Entity, !entity.items.isEmpty else {
                return nil
            }
            self.init(entity, for: event)
        }
        
        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            return LayoutSpec().then {
                $0.spacing = 4
                $0.children = subnodes
                $0.singleLayoutSize = .init(min: .init(width: 170, height: 170),
                                            max: .init(width: 0, height: 280))
            }
        }
        
        /// Craete a badge node with value.
        static func badge(_ value: String) -> ASDisplayNode {
            let badge = ASButtonNode()
            
            // 6226
            badge.isUserInteractionEnabled = false
            badge.isLayerBacked = true
            badge.setBackgroundImage(#imageLiteral(resourceName: "cm4_act_tag.png"), for: .normal)
            badge.style.minSize = .init(width: 33, height: 16)
            badge.setTitle(value, with: UIFont.systemFont(ofSize: 10), with: .white, for: .normal)

            return badge
        }
    }
    
    /// The video node for event.
    class Video<Source>: NMOverviewVideoNode, NMEventNodeDisplayable, NMEventControllerAutoPlayable where Source: Decodable {

        let plays: ASButtonNode = .init()
        let duration: ASButtonNode = .init()

        var isPlayable: Bool {
            return notice == nil
        }
        
        override var playerState: ASVideoNodePlayerState {
            willSet {
                switch newValue {
                case .playing:
                    startWaving()

                default:
                    stopWaving()
                }
            }
        }

        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Video<Source>, for event: NMEvent) {
            super.init()
            
            self.cornerRadius = 8
            self.clipsToBounds = true
            
            self.shouldAutoplay = false
            self.shouldAutorepeat = false
            self.shouldAggressivelyRecoverFromStall = false
            
            self.muted = true
            self.assetURL = entity.url
            self.placeholderImageURL = entity.cover
            self.periodicTimeObserverTimescale = 4

            self.footerNode = NMGradientNode().then {

                $0.style.height = ASDimensionMakeWithPoints(100)
                $0.colors = [
                    UIColor(white: 0, alpha: 0.0),
                    UIColor(white: 0, alpha: 0.4),
                ]
                
                $0.addSubnode(self.plays) {
                    $0.isUserInteractionEnabled = false
                    $0.isLayerBacked = true
                    $0.contentSpacing = 2
                    $0.setAttributedTitle(self.attributedStringOfViews(entity.plays, style: event.theme["event.video.plays"]), for: .normal)
                    $0.setImage(#imageLiteral(resourceName: "cm4_cover_icn_video"), for: .normal)
                }
                
                $0.addSubnode(self.duration) {
                    $0.isUserInteractionEnabled = false
                    $0.isLayerBacked = true
                    $0.contentSpacing = 2
                    $0.setAttributedTitle(self.attriubtedStringOfDuration(entity.duration, style: event.theme["event.video.duration"]), for: .normal)
                    $0.setImage(#imageLiteral(resourceName: "cm4_act_video_run_1"), for: .normal)
                }
                
                $0.layoutSpecBlock = { node, _ in
                    let spec = ASStackLayoutSpec.horizontal()
                    spec.children = node.subnodes
                    spec.justifyContent = .spaceBetween
                    return spec.insets(top: .infinity, left: 6, bottom: 4, right: 6)
                }
            }
            self.headerNode = NMGradientNode().then {
                // If the source not is movie, not need to display the header node.
                guard let movie = entity.source as? NMMedia.Movie else {
                    $0.isHidden = true
                    return
                }

                $0.style.height = ASDimensionMakeWithPoints(100)
                $0.colors = [
                    UIColor(white: 0, alpha: 0.4),
                    UIColor(white: 0, alpha: 0.0),
                ]
                
                $0.addSubnode(ASTextNode()) {
                    $0.isUserInteractionEnabled = false
                    $0.isLayerBacked = true
                    $0.maximumNumberOfLines = 1
                    $0.truncationMode = .byTruncatingTail
                    $0.attributedText = NSMutableAttributedString(string: "{MV}\(movie.name)", style: event.theme["event.video.title"])
                }
                $0.addSubnode(ASTextNode()) {
                    $0.isUserInteractionEnabled = false
                    $0.isLayerBacked = true
                    $0.maximumNumberOfLines = 1
                    $0.truncationMode = .byTruncatingTail
                    $0.attributedText = NSMutableAttributedString(string: movie.artist.name, style: event.theme["event.video.subtitle"])
                }
                
                $0.layoutSpecBlock = { node, _ in
                    let spec = ASStackLayoutSpec.vertical()
                    spec.children = node.subnodes
                    return spec.insets(top: 4, left: 6, bottom: .infinity, right: 6)
                }
            }
        }
        
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            // In an error occurs case, only display the notice node.
            if let notice = notice {
                return notice.insets()
            }
            
            // In normal case display all contents.
            return ASRatioLayoutSpec(ratio: 9 / 16, child: super.layoutSpecThatFits(constrainedSize))
        }
        

        override func videoNode(_ videoNode: ASVideoNode, didPlayToTimeInterval timeInterval: TimeInterval) {
            super.videoNode(videoNode, didPlayToTimeInterval: timeInterval)
            
            // If waving is stoped, ignore.
            guard duration.isSelected else {
                return
            }
            // The countdown can't be displayed until the time has been successfully obtained.
            videoNode.currentItem.map {
                duration.titleNode.attributedText = attriubtedStringOfDuration(fmax($0.duration.seconds - timeInterval, 0))
            }
        }
        
        private func startWaving() {
            // If waving is started, ignore.
            guard !duration.isSelected else {
                return
            }
            duration.isSelected = true
            duration.imageNode.animatedImage = NMAnimatedImage((1 ... 4).map({ "cm4_act_video_run_\($0)" }), duration: 0.6)
        }
        private func stopWaving() {
            // If waving is stoped, ignore
            guard duration.isSelected else {
                return
            }
            duration.imageNode.image = nil
            duration.imageNode.animatedImage = nil
            duration.titleNode.attributedText = nil
            duration.isSelected = false // Changing button state will reset all contents.
        }
        
        private func attributedStringOfViews(_ count: Int, style: NMTheme.Style? = nil) -> NSAttributedString? {
            
            guard let style = style ?? styleOfViews else {
                return nil
            }
            styleOfViews = style

            var text = "\(count)"
            if count > 100_000 {
                text = "\(count / 10_000)万"
            }
            
            return NSMutableAttributedString(string: text, style: style)
        }
        private func attriubtedStringOfDuration(_ seconds: TimeInterval, style: NMTheme.Style? = nil) -> NSAttributedString? {
            
            guard let style = style ?? styleOfDuration else {
                return nil
            }
            styleOfDuration = style
            
            let text = String(format: "%02zd:%02zd", Int(seconds) / 60, Int(seconds) % 60)
            return NSMutableAttributedString(string: text, style: style)
        }

        private var notice: ASDisplayNode?
        
        private var styleOfViews: NMTheme.Style?
        private var styleOfDuration: NMTheme.Style?
    }
    
    /// The music node for event.
    class Music<Source>: NMDimmingNode, NMEventNodeDisplayable where Source: Decodable {
        
        let title: ASTextNode = .init()
        let subtitle: ASTextNode = .init()
        
        let playback: ASImageNode = .init()
        let image: NMNetworkImageNode = .init()
        let mask: ASImageNode = .init()

        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Music<Source>, for event: NMEvent) {
            super.init()
            
            self.cornerRadius = 8
            self.backgroundColor = event.theme.color(forKey: "event.music.background")

            self.addSubnode(self.title) {
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                $0.truncationMode = .byTruncatingTail
                $0.maximumNumberOfLines = 1
                $0.attributedText = NSMutableAttributedString(string: entity.title, style: event.theme["event.music.title"])
            }
            
            self.addSubnode(self.subtitle) {
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                $0.truncationMode = .byTruncatingTail
                $0.maximumNumberOfLines = 1
                $0.attributedText = NSMutableAttributedString(string: entity.subtitle, style: event.theme["event.music.subtitle"])
            }
            
            self.addSubnode(self.image) {
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                $0.style.preferredSize = .init(width: 40, height: 40)
                $0.cornerRadius = 4
                $0.url = entity.cover
            }
            
            
            switch entity.source {
            case is NMMedia.Song,
                 is NMMedia.Program:
                // song: title(name + (transNames[0]))/subtitle(artists.name)/cover(album.picUrl)/has play
                self.addSubnode(self.playback) {
                    $0.isLayerBacked = true
                    $0.style.preferredSize = .init(width: 20, height: 20)
                    $0.image = #imageLiteral(resourceName: "cm2_list_cover_radio_play")
                }

            case is NMMedia.Album:
                // album: title(album.name)/subtitle(album.artist.name)/cover(album.picUrl)/no play
                self.addSubnode(self.mask) {
                    $0.isLayerBacked = true
                    $0.image = #imageLiteral(resourceName: "cm2_act_cover_alb")
                }
                
            case is NMMedia.Show:
                
                self.title.maximumNumberOfLines = 2
                self.image.style.preferredSize = .init(width: 40, height: 54)
                                
            default:
                break
            }
        }

        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            
            // Group 1 is album style, using image and mask layer.
            let group1 = mask.supernode.map { _ in
                ASOverlayLayoutSpec.overlay(
                    image.insets(right: 7),
                    mask
                )
            }
            // Group 2 is song style, using image and icon layer.
            let group2 = playback.supernode.map { _ in
                ASOverlayLayoutSpec.overlay(
                    image,
                    ASCenterLayoutSpec(centeringOptions: .XY, child: playback)
                )
            }
           
            let spec = ASStackLayoutSpec.horizontal(
                group1 ?? group2 ?? image,
                ASStackLayoutSpec.vertical(title, subtitle, spacing: 2, verticalAlignment: .center).then {
                    $0.style.flexShrink = 1
                    $0.style.flexGrow = 1
                },
                spacing: 8
            )
            
            return spec.insets(top: 8, left: 8, bottom: 8, right: 8)
        }
    }
    
    /// The referenced node for event.
    class Referenced: NMDimmingNode, NMEventNodeDisplayable {

        let tiles: [ASDisplayNode]

        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Referenced, for event: NMEvent) {
            // Load all tile display node.
            self.tiles = NMEventNode.Display.parse(entity.event.entitys, for: entity.event)
            super.init()
            
            self.cornerRadius = 8
            self.backgroundColor = event.theme.color(forKey: "event.referenced.background")
            
            self.tiles.forEach {
                self.addSubnode($0)
            }
        }
        
        /// Setup layout for all subnodes.
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {

            let spec = ASStackLayoutSpec.vertical().then {
                $0.spacing = 6
                $0.children = tiles
            }
            
            return spec.insets(top: 8, left: 8, bottom: 8, right: 8)
        }
        
        private var notice: ASDisplayNode?
    }
}


// MARK: -


extension NMEventNode.Display {
    
    /// The music log content entity.
    class Log: ASDisplayNode, NMEventNodeDisplayable {
        
        let cover: NMNetworkImageNode = .init()
        let header: NMGradientNode = .init()

        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Log, for event: NMEvent) {
            super.init()
            
            self.addSubnode(self.cover) {
                
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                
                $0.cornerRadius = 4
                $0.contentMode = .scaleAspectFill
                $0.url = entity.log.cover
            }
            self.addSubnode(self.header) {
                
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                
                $0.cornerRadius = 4
                $0.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
                $0.colors = [
                    UIColor(white: 0, alpha: 0.4),
                    UIColor(white: 0, alpha: 0.0)
                ]
                
                $0.addSubnode(ASImageNode()) {
                    $0.isLayerBacked = true
                    $0.image = #imageLiteral(resourceName: "cm6_square_event_mlog")
                }
                $0.addSubnode(ASImageNode()) {
                    $0.isLayerBacked = true
                    $0.isHidden = entity.log.type != 2 // Is a video log?
                    $0.image = #imageLiteral(resourceName: "cm6_square_feed_video")
                }
                
                $0.style.height = ASDimensionMakeWithPoints(30)
                $0.layoutSpecBlock = { node, _ in
                    let spec = ASStackLayoutSpec.horizontal()
                    spec.children = node.subnodes
                    spec.justifyContent = .spaceBetween
                    return spec.insets(top: 6, left: 6, bottom: 6, right: 6)
                }
            }
        }
        
        
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            let spec = ASRatioLayoutSpec(ratio: 1.3, child: cover)
            spec.style.width = ASDimensionMakeWithFraction(3 / 5)
            let overlay = ASOverlayLayoutSpec(child: spec, overlay: header.insets(bottom: .infinity))
            return overlay.insets(right: .infinity)
        }
    }

    /// The comment content entity.
    class Quoted: ASImageNode, NMEventNodeDisplayable {
        
        let quote: NMTextNode = .init()
        
        let footer: ASDisplayNode = .init()
        let separator: ASDisplayNode = .init()
        

        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Quoted, for event: NMEvent) {
            super.init()
            
            self.image = #imageLiteral(resourceName: "cm4_act_quote")
            self.imageModificationBlock = ASImageNodeTintColorModificationBlock(event.theme.color(forKey: "event.quote.separator"))

            self.addSubnode(self.quote) {
                let title = "@\(entity.contents.sender.nickname) 回复：\(entity.contents.contents)"
                $0.isLayerBacked = true
                $0.truncationMode = .byTruncatingTail
                $0.attributedText = NSMutableAttributedString(string: title, style: event.theme["event.quote.title"]).then {
                    let style = NSMutableParagraphStyle()
                    style.lineSpacing = 8
                    $0.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, $0.length))
                }
            }
            
            self.addSubnode(self.footer) {
                
                $0.isLayerBacked = true
                
                $0.style.height = ASDimensionMake(62)
                
                $0.addSubnode(NMNetworkImageNode()) {
                    
                    $0.isLayerBacked = true
                    $0.url = entity.cover
                    $0.cornerRadius = 3
                    $0.contentMode = .scaleAspectFill
                    $0.style.preferredSize = .init(width: 30, height: 30)
                }
                
                $0.addSubnode(ASTextNode()) {
                    
                    $0.isLayerBacked = true
                    $0.truncationMode = .byTruncatingTail
                    $0.maximumNumberOfLines = 1
                    $0.style.flexShrink = 1
                    $0.attributedText = NSMutableAttributedString(string: entity.name, style: event.theme["event.quote.source"])
                }
                
                $0.layoutSpecBlock = { node, _ in
                    let spec = ASStackLayoutSpec.horizontal(node.subnodes, spacing: 10)
                    spec.alignItems = .center
                    return spec.insets(left: 16, right: 16)
                }
            }

            guard let replies = entity.contents.replies, !replies.isEmpty else {
                self.replies = []
                return
            }
            
            self.replies = replies.map {
                let node = NMTextNode()
                let title: String
                switch $0.status {
                case -1:
                    title = "该评论己删除"
                    
                case -10:
                    title = "该评论涉及违规内容，经举报己被屏蔽"

                default:
                    title = "@\($0.sender.nickname)：\($0.contents)"
                }
                
                node.isLayerBacked = true
                node.truncationMode = .byTruncatingTail
                node.style.flexShrink = 1
                node.attributedText = NSMutableAttributedString(string: title, style: event.theme["event.quote.subtitle"]).then {
                    let style = NSMutableParagraphStyle()
                    style.lineSpacing = 8
                    $0.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, $0.length))
                }
                
                self.addSubnode(node)
                return node
            }
            self.addSubnode(self.separator) {
                $0.isLayerBacked = true
                $0.style.width = ASDimensionMake(2)
                $0.backgroundColor = event.theme["event.quote.separator"].color
            }
        }
        
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            // The default blank line is 7px.
            let spec = ASStackLayoutSpec.vertical(quote, spacing: 7)

            // Replies node need to be added to the hierarchy when they exist.
            if !replies.isEmpty {
                let subspec = ASStackLayoutSpec.vertical(replies, spacing: 8).insets(top: 3, left: 10, bottom: 3)
                subspec.style.flexGrow = 1
                subspec.style.flexShrink = 1
                spec.children?.append(ASStackLayoutSpec.horizontal(separator, subspec))
            }

            return ASStackLayoutSpec.vertical(spec.insets(top: 30, left: 15, right: 15), footer, spacing: 5)
        }
        
        static func exception(_ entity: NMEvent.Exception, for event: NMEvent) throws -> ASDisplayNode {
            let node = ASButtonNode()
            
            node.backgroundImageNode.image = #imageLiteral(resourceName: "cm4_act_quote")
            node.backgroundImageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(rgba(0x26ffffff))
            
            node.isUserInteractionEnabled = false
            node.isLayerBacked = true
            
            node.contentEdgeInsets = .init(top: 24, left: 15, bottom: 15, right: 15)
            node.setAttributedTitle(NSMutableAttributedString(string: entity.error.localizedDescription, style: event.theme["event.deleted"]), for: .normal)

            return node
        }
        
        private var replies: [ASDisplayNode] = .init()
    }
    
    /// The share content entity.
    class Shared<Source>: NMDimmingNode, NMEventNodeDisplayable where Source: Decodable {
        
        let title: ASTextNode = .init()
        let subtitle: ASTextNode = .init()
        
        let cover: NMNetworkImageNode = .init()
        
        /// Create a display node using entity and events.
        required init(_ entity: NMEvent.Shared<Source>, for event: NMEvent) {
            super.init()
            
            self.cornerRadius = 8
            self.backgroundColor = event.theme.color(forKey: "event.shared.background")
            
            self.addSubnode(self.title) {
                $0.isLayerBacked = true
                $0.maximumNumberOfLines = 2
                $0.truncationMode = .byTruncatingTail
                $0.attributedText = NSMutableAttributedString(string: entity.title, style: event.theme["event.shared.title"])
            }
            self.addSubnode(self.subtitle) {
                $0.isLayerBacked = true
                $0.maximumNumberOfLines = 1
                $0.truncationMode = .byTruncatingTail
                $0.attributedText = NSMutableAttributedString(string: entity.subtitle, style: event.theme["event.shared.subtitle"])
            }

            self.addSubnode(self.cover) {
                
                $0.isUserInteractionEnabled = false
                $0.isLayerBacked = true
                $0.cornerRadius = 4
                $0.url = entity.cover
            }
        }
        
        override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            
            let spec = ASStackLayoutSpec.horizontal()
            
            spec.verticalAlignment = .center
            spec.spacing = 8
            spec.children = [
                ASRatioLayoutSpec(ratio: 2 / 3, child: cover),
                ASStackLayoutSpec.vertical().then {
                    $0.spacing = 7
                    $0.children = [title, subtitle]
                    $0.style.flexShrink = 1
                }
            ]
            spec.style.height = ASDimensionMake(86)

            return spec.insets(top: 8, left: 8, bottom: 8, right: 8)
        }
    }
}


// MARK: -


protocol NMEventNodeDecodable {
    
    /// Creates a new instance by decoding from the given decoder.
    init?(from decoder: Decoder) throws
}

/// The protocol specifies the display node for the entity object binding.
protocol NMEventNodeEntity: NMEventNodeEntityCompatible where Display: NMEventNodeDisplayableCompatible {
    /// The node type that is actually displayed.
    associatedtype Display
}

/// The protocol specifies the entity object  for the display node binding.
protocol NMEventNodeDisplayable: NMEventNodeDisplayableCompatible where Entity: NMEventNodeEntityCompatible {
    /// The entity type that is actually.
    associatedtype Entity
    
    /// Provides a way to create a display node using a real entity object.
    init(_ entity: Entity, for event: NMEvent)
}

/// Same as NMEventNodeEntity.
protocol NMEventNodeEntityCompatible {
    
    /// Provides a type method to get the display node for the current binding.
    var displayType: NMEventNodeDisplayableCompatible.Type { get }
}

/// Same as NMEventNodeDisplayable.
protocol NMEventNodeDisplayableCompatible {
    
    /// If the entity object is not compatible with the object required by the display node, it will return nil.
    init?(_ entity: NMEventNodeEntityCompatible, for event: NMEvent)
    
    /// When an exception occurs on a entity, should call this method to create the contents of exception.
    static func exception(_ entity: NMEvent.Exception, for event: NMEvent) throws -> ASDisplayNode
}

extension NMEventNodeEntity {
    
    /// Provides the default type binding.
    var displayType: NMEventNodeDisplayableCompatible.Type {
        return Display.self
    }
}

extension NMEventNodeDisplayable {
    
    /// Provide a default convert implementation.
    init?(_ entity: NMEventNodeEntityCompatible, for event: NMEvent) {
        // If the conversion fails, it means incompatibility.
        guard let entity = entity as? Entity else {
            return nil
        }
        self.init(entity, for: event)
    }
    
    /// Provide a default implementation for contents of exception.
    static func exception(_ entity: NMEvent.Exception, for event: NMEvent) throws -> ASDisplayNode {
        let node = ASButtonNode()
        
        node.cornerRadius = 4
        node.backgroundColor = event.theme.color(forKey: "event.deleted.background")
        
        node.isUserInteractionEnabled = false
        node.isLayerBacked = true
        
        node.style.height = ASDimensionMake(45)
        
        node.setAttributedTitle(NSMutableAttributedString(string: entity.error.localizedDescription, style: event.theme["event.deleted"]), for: .normal)
        
        return node
    }
}


// MARK: -


fileprivate extension ASDisplayNode {

    func addSubnode<T: ASDisplayNode>(_ subnode: T, before: (T) -> Void) {
        before(subnode)
        addSubnode(subnode)
    }
}


